package com.tyros.common.secure.password;

import com.tyros.common.secure.CryptoException;
import org.bouncycastle.crypto.generators.BCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Bcrypt加密算法<br>copy from spring-security
 *
 * @author zsy
 */
@SuppressWarnings("ALL")
public class BcryptPasswordEncoder implements PasswordEncoder {

    private static final Pattern BCRYPT_PATTERN = Pattern.compile("^\\$2a?\\$\\d{2}\\$.+");

    private final Logger logger = LoggerFactory.getLogger(BcryptPasswordEncoder.class);

    private final int strength;

    private final SecureRandom random;

    private static final char[] BASE64_CODE = new char[]{'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    private static final byte[] INDEX_64 = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1};


    public BcryptPasswordEncoder() {
        // 配置为10大概用时70ms
        this(10);
    }

    public BcryptPasswordEncoder(int strength) {
        this(strength, new SecureRandom());
    }

    public BcryptPasswordEncoder(int strength, SecureRandom random) {
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    @Override
    public String encode(String rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            return hash(rawPassword, genSalt(null));
        }
    }

    @Override
    public String encode(String rawPassword, byte[] salt) {
        return hash(rawPassword, genSalt(salt));
    }

    /**
     * 生成随机盐值
     *
     * @return 盐值字符
     */
    public String genSalt(byte[] bytes) {
        StringBuilder rs = new StringBuilder();
        byte[] rnd = bytes;
        if (bytes == null || bytes.length != 16) {
            rnd = new byte[16];
            random.nextBytes(rnd);
        }
        rs.append("$2a$");
        if (strength < 10) {
            rs.append("0");
        }
        rs.append(strength);
        rs.append("$");
        base64Encode(rnd, rnd.length, rs);
        return rs.toString();
    }

    @Override
    public boolean matches(String rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return encodedPassword.equals(hash(rawPassword, encodedPassword));
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

    public String hash(String password, String salt) throws IllegalArgumentException {
        char minor = '0';
        StringBuilder rs = new StringBuilder();
        if (salt == null) {
            throw new CryptoException("salt cannot be null");
        } else {
            int saltLength = salt.length();
            if (saltLength < 28) {
                throw new CryptoException("Invalid salt");
            } else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
                byte off;
                if (salt.charAt(2) == '$') {
                    off = 3;
                } else {
                    minor = salt.charAt(2);
                    if (minor != 'a' || salt.charAt(3) != '$') {
                        throw new CryptoException("Invalid salt revision");
                    }
                    off = 4;
                }
                if (saltLength - off < 25) {
                    throw new CryptoException("Invalid salt");
                } else if (salt.charAt(off + 2) > '$') {
                    throw new CryptoException("Missing salt rounds");
                } else {
                    int rounds = Integer.parseInt(salt.substring(off, off + 2));
                    String cipherText = salt.substring(off + 3, off + 25);
                    byte[] saltBytes = base64Decode(cipherText, 16);
                    rs.append("$2");
                    if (minor >= 'a') {
                        rs.append(minor);
                    }
                    rs.append("$");
                    if (rounds < 10) {
                        rs.append("0");
                    }
                    rs.append(rounds);
                    rs.append("$");
                    byte[] passwordBytes = (password + (minor >= 'a' ? "\u0000" : "")).getBytes(StandardCharsets.UTF_8);
                    byte[] generate = BCrypt.generate(passwordBytes, saltBytes, rounds);
                    base64Encode(saltBytes, saltBytes.length, rs);
                    base64Encode(generate, 23, rs);
                    return rs.toString();
                }
            } else {
                throw new CryptoException("Invalid salt version");
            }
        }
    }

    /**
     * 拆分密文
     *
     * @param cipherText 密文
     * @return arrays[0]=密文算法摘要;arrays[1]=密文散列值
     */
    @Override
    public String[] split(String cipherText) {
        Matcher matcher = BCRYPT_PATTERN.matcher(cipherText);
        if (!matcher.matches()) {
            throw new CryptoException("Invalid cipher text format");
        }
        String actualCipherText = cipherText.substring(cipherText.lastIndexOf("$") + 1);
        String[] arrays = new String[2];
        arrays[0] = cipherText.substring(0, cipherText.lastIndexOf("$") + 1) + actualCipherText.substring(0, 22);
        arrays[1] = actualCipherText.substring(22);
        return arrays;
    }


    /**
     * 合并密文
     *
     * @param header  密文头信息
     * @param payload 密文负载信息
     * @return 密文原始信息
     */
    @Override
    public String merge(String header, String payload) {
        Matcher matcher = BCRYPT_PATTERN.matcher(header + payload);
        if (!matcher.matches()) {
            throw new CryptoException("Invalid cipher text format");
        }
        return header + payload;
    }

    static void base64Encode(byte[] d, int len, StringBuilder rs) throws IllegalArgumentException {
        int off = 0;
        if (len > 0 && len <= d.length) {
            while (off < len) {
                int c1 = d[off++] & 255;
                rs.append(BASE64_CODE[c1 >> 2 & 63]);
                c1 = (c1 & 3) << 4;
                if (off >= len) {
                    rs.append(BASE64_CODE[c1 & 63]);
                    break;
                }

                int c2 = d[off++] & 255;
                c1 |= c2 >> 4 & 15;
                rs.append(BASE64_CODE[c1 & 63]);
                c1 = (c2 & 15) << 2;
                if (off >= len) {
                    rs.append(BASE64_CODE[c1 & 63]);
                    break;
                }

                c2 = d[off++] & 255;
                c1 |= c2 >> 6 & 3;
                rs.append(BASE64_CODE[c1 & 63]);
                rs.append(BASE64_CODE[c2 & 63]);
            }

        } else {
            throw new IllegalArgumentException("Invalid len");
        }
    }

    private static byte char64(char x) {
        return x > INDEX_64.length ? -1 : INDEX_64[x];
    }

    static byte[] base64Decode(String s, int maxolen) throws IllegalArgumentException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(maxolen);
        int off = 0;
        int slen = s.length();
        int olen = 0;
        if (maxolen <= 0) {
            throw new IllegalArgumentException("Invalid maxolen");
        } else {
            while (off < slen - 1 && olen < maxolen) {
                byte c1 = char64(s.charAt(off++));
                byte c2 = char64(s.charAt(off++));
                if (c1 == -1 || c2 == -1) {
                    break;
                }

                byte o = (byte) (c1 << 2);
                o = (byte) (o | (c2 & 48) >> 4);
                out.write(o);
                ++olen;
                if (olen >= maxolen || off >= slen) {
                    break;
                }

                byte c3 = char64(s.charAt(off++));
                if (c3 == -1) {
                    break;
                }

                o = (byte) ((c2 & 15) << 4);
                o = (byte) (o | (c3 & 60) >> 2);
                out.write(o);
                ++olen;
                if (olen >= maxolen || off >= slen) {
                    break;
                }

                byte c4 = char64(s.charAt(off++));
                o = (byte) ((c3 & 3) << 6);
                o |= c4;
                out.write(o);
                ++olen;
            }

            return out.toByteArray();
        }
    }
}
